﻿using System;
using System.Net;
using System.Net.Sockets;

namespace Oblivion.Networking
{
    /// <summary> This class encapsulates methods and events relating to the socket system. </summary>
    public sealed class ServerSocket : Socket
    {
        // Class-Scope Constant Declarations:
        private const int PACKET_BUFFER_LENGTH = 864;

        // Global-Scope Socket Event Declarations:
        public Action<Passport> OnClientConnect; // Executed on client connection, create the client.
        public Action<Passport> OnClientDisconnect; // Executed on client disconnect, trash the client.
        public Action<Passport, byte[]> OnClientReceive; // Executed on client receive when it wants to handle a packet.

        /// <summary>
        /// This class contains an asynchronous socket system based on an implementation of Berkeley's socket interface,
        /// designed to accept new connections to the server and process all incoming data from the client. This socket 
        /// system is optimized to run with any server that does not require a socket event for exchanging keys.
        /// </summary>
        public ServerSocket() // Default Constructor
            : base(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
        {
            // Define the server's socket options:
            base.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true);
            base.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);

            // You can set this to Edge Restricted to do some pretty cool things locally (without the worry of an outside 
            // connection coming in and trying to hack into the socket system). Edge = local network.
            base.SetIPProtectionLevel(IPProtectionLevel.Unrestricted);
        }

        /// <summary> This method associates the socket system with an IP End Point. </summary>
        /// <param name="ipAddress">The ip address that the socket system should listen with.</param>
        /// <param name="port">The port that the socket system should listen with.</param>
        public bool Bind(string ipAddress, int port)
        {
            // Bind the server:
            try
            {
                base.Bind(new IPEndPoint(IPAddress.Parse(ipAddress), port));
                return true;
            }
            catch (Exception e) /* throws an error if the address is in use. */
            {
                Console.WriteLine(e);
                return false;
            }
        }

        /// <summary> This method places the socket system into the listening state. </summary>
        /// <param name="backlog">The maximum length of the pending connections queue.</param>
        public new void Listen(int backlog)
        {
            // Check to see if the socket events have been assigned actions.
            if (OnClientConnect == null || OnClientReceive == null || OnClientDisconnect == null)
                throw new NullReferenceException("The socket events have not been defined!");

            // Start listening and accepting new connections:
            base.Listen(backlog);
            base.BeginAccept(Connect, null); // null = result.AsyncState 
        }

        /// <summary>
        /// This method handles incoming connections from the pending connections queue. To accept a new client,
        /// we use the end accept method. Then, we tell the server to begin receiving packets from the client.
        /// </summary>
        public void Connect(IAsyncResult result) // result.AsyncState is null from the BeginAccept invoke.
        {
            try
            {
                // Take the accepted connection and create a passport out of it.
                var passport = new Passport(base.EndAccept(result));
                base.BeginAccept(Connect, null); // Start accepting clients again on another thread.

                // Create the client:
                OnClientConnect(passport);
                passport.Buffer = new byte[PACKET_BUFFER_LENGTH];
                passport.Socket.BeginReceive(passport.Buffer, 0, PACKET_BUFFER_LENGTH, SocketFlags.None, Receive,
                                             passport);
            }
            catch (SocketException e)
            {
                // Handle *ALL* the errors! :D
                if (e.SocketErrorCode != SocketError.Disconnecting &&
                    e.SocketErrorCode != SocketError.ConnectionReset &&
                    e.SocketErrorCode != SocketError.ConnectionAborted &&
                    e.SocketErrorCode != SocketError.NotConnected)
                    Console.WriteLine(e);

                // If the base is still connected (the server is still running), attempt to continue accepting clients:
                if (base.Connected) base.BeginAccept(Connect, null);
            }
        }

        /// <summary>
        /// This method handles incoming data from an individual client's socket connection. The data is encrypted
        /// using the NetDragon Asymmetric Authentication Cipher.
        /// </summary>
        /// <param name="result">The result of the asynchronous operation - contains the passport as "AsyncState".</param>
        public void Receive(IAsyncResult result)
        {
            // Get and error check the client (treat asyncState as a type of Passport):
            var passport = result.AsyncState as Passport;
            if (passport == null || passport.Socket == null || !passport.Socket.Connected)
                return;

            try
            {
                // Retrieve the length of the packet, end the receive, & decrypt the buffer:
                int length = passport.Socket.EndReceive(result);
                if (length > 0)
                {
#warning Fix!
                    Array.Resize(ref passport.Buffer, length);

                    //byte[] buffer = { 0 }; // Was originally the decrypted packet from CO.

                    byte[] buffer = passport.Buffer;

                    //Removed due to it was optimzed for CO and does not work with minecraft

                    //// At this point, we have the decrypted buffer and we know it's either one whole packet or a fragment 
                    //// of a packet. We need to check if the packet is a fragment.
                    //if (buffer.Length > 3 && BitConverter.ToUInt16(buffer, 0) /* the packet's reported length */ > length)
                    //{
                    //    // This packet is a fragment. Let's resize the buffer to match the length of the packet.
                    //    int expectedLength = BitConverter.ToUInt16(buffer, 0);

                    //    // Tell the client's socket to start receiving the rest of the packet:
                    //    // Good thing we don't touch the passport's buffer when we decrypt it! It's still encrypted so we can get the rest!
                    //    passport.Socket.BeginReceive(passport.Buffer, length, expectedLength - length, SocketFlags.None, Receive, passport);
                    //    return;
                    //}

                    OnClientReceive(passport, buffer);

                    // Receive the next packet!
                    passport.Buffer = new byte[PACKET_BUFFER_LENGTH];
                    passport.Socket.BeginReceive(passport.Buffer, 0, PACKET_BUFFER_LENGTH, SocketFlags.None, Receive,
                                                 passport);
                }
                else if (passport.Socket != null && passport.Socket.Connected)
                    passport.Socket.BeginDisconnect(true, Disconnect, passport);
            }
            catch (SocketException e)
            {
                // Was the connection issue a problem on our side or the client's side?
                if (e.SocketErrorCode != SocketError.Disconnecting &&
                    e.SocketErrorCode != SocketError.ConnectionReset &&
                    e.SocketErrorCode != SocketError.ConnectionAborted &&
                    e.SocketErrorCode != SocketError.NotConnected)
                    Console.WriteLine(e);
            }
        }

        /// <summary> This method handles disconnecting clients. </summary>
        /// <param name="result">The status of the Asynchronous connection.</param>
        public void Disconnect(IAsyncResult result)
        {
            try
            {
                // If the client still exists, dispose of last minute things:
                var passport = result.AsyncState as Passport;
                if (passport != null && passport.Client != null)
                {
                    OnClientDisconnect(passport);
                    passport.Client = null;
                }
                passport = null;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}